本文同步更新於blog
需求一:客戶想要一台收銀機
<?php
namespace App\StrategyPattern\CashRegister;
class Program
{
/**
* @var int
*/
private $originalPrice;
public function __construct($originalPrice)
{
$this->originalPrice = $originalPrice;
}
public function pay()
{
return $this->originalPrice;
}
}
需求二:客戶想要有一個優惠活動 (打8折)
<?php
namespace App\StrategyPattern\CashRegister;
class Program
{
/**
* @var int
*/
private $originalPrice;
/**
* @var string
*/
private $promotion;
public function __construct($originalPrice, $promotion)
{
$this->originalPrice = $originalPrice;
$this->promotion = $promotion;
}
public function pay()
{
if ($this->promotion == '20% off') {
return $this->originalPrice * 0.8;
}
return $this->originalPrice;
}
}
需求三:客戶想要有另一個優惠 (買300回饋100)
public function pay()
{
$originalPrice = $this->originalPrice;
if ($this->promotion == '20% off') {
return $originalPrice * 0.8;
}
if ($this->promotion == 'spend_300_feedback_100') {
if ($originalPrice >= 300) {
return $originalPrice - floor($originalPrice / 300) * 100;
}
}
return $originalPrice;
}
這時候功能是完成了,但有沒有覺得哪裡怪怪的?
欸嘿,我們想到之前學過的簡單工廠模式。
可以實作三個類別,分別是正常付費、8折付費、買300回饋100。
讓我們利用簡單工廠改造它。
<?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Payable
{
public function pay();
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class NormalPay implements Payable
{
/**
* @var int
*/
private $originalPrice;
public function __construct($originalPrice)
{
$this->originalPrice = $originalPrice;
}
public function pay()
{
return $this->originalPrice;
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class OffPercentPay implements Payable
{
/**
* @var int
*/
private $originalPrice;
/**
* @var double
*/
private $offPercent;
public function __construct($originalPrice, $offPercent)
{
$this->originalPrice = $originalPrice;
$this->offPercent = $offPercent;
}
public function pay()
{
return $this->originalPrice * (1 - $this->offPercent);
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class FeedbackPay implements Payable
{
/**
* @var int
*/
private $originalPrice;
/**
* @var int
*/
private $priceCondition;
/**
* @var int
*/
private $feedback;
public function __construct($originalPrice, $priceCondition, $feedback)
{
$this->originalPrice = $originalPrice;
$this->priceCondition = $priceCondition;
$this->feedback = $feedback;
}
public function pay()
{
$originalPrice = $this->originalPrice;
$priceCondition = $this->priceCondition;
$feedback = $this->feedback;
if ($originalPrice >= $priceCondition) {
return $originalPrice - floor($originalPrice / $priceCondition) * $feedback;
}
return $originalPrice;
}
}
最後原本程式再搭配工廠即可完成。 (下略)
正當我們洋洋得意的時候,客戶送來第四個需求...
需求四:客戶希望收銀機可以開一般發票或電子發票
不是啊,客戶你要這種發票類型的需求你要先說.. (碎念)
按照簡單工廠模式的思維,
我們必須為這個需求做出6個類別,
分別是(正常付費、打折付費、買多少回饋多少)x(一般發票、電子發票)的排列組合。
不行不行,假設客戶將來又提出要開統編的需求,我們就要寫8個類別了。
而且這樣也違反開放封閉原則。
每次有新需求都會改動所有的程式碼。
在我們研究一下後,發現了一個適合的設計模式:策略模式
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\OffPercentPay;
use App\StrategyPattern\CashRegister\FeedbackPay;
use App\StrategyPattern\CashRegister\NormalPay;
use App\StrategyPattern\CashRegister\Contracts\Payable;
class CashContext
{
/**
* @var Payable
*/
private $discountMethod;
/**
* @param int $originalPrice
* @param string $discountType
*/
public function __construct($originalPrice, $discountType)
{
$this->resolveDiscountMethod($originalPrice, $discountType);
}
/**
* @param int $originalPrice
* @param string $discountType
*/
private function resolveDiscountMethod($originalPrice, $discountType)
{
switch ($discountType) {
case '20% off':
$this->discountMethod = new OffPercentPay($originalPrice, 0.2);
break;
case 'spend_300_feedback_100':
$this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
break;
default:
$this->discountMethod = new NormalPay($originalPrice);
break;
}
}
public function pay()
{
return $this->discountMethod->pay();
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program
{
/**
* @var CashContext
*/
private $cashContext;
/**
* @param int $originalPrice
* @param string $discountType
*/
public function __construct($originalPrice, $discountType)
{
$this->cashContext = new CashContext($originalPrice, $discountType);
}
public function pay()
{
return $this->cashContext->pay();
}
}
這樣好像還看不出來有什麼好處,我們繼續實作。
<?php
namespace App\StrategyPattern\CashRegister\Contracts;
interface Receiptable
{
public function getReceipt();
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class NormalReceipt implements Receiptable
{
public function getReceipt()
{
return '一般發票';
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class ElectronicReceipt implements Receiptable
{
public function getReceipt()
{
return '電子發票';
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\NormalPay;
use App\StrategyPattern\CashRegister\NormalReceipt;
use App\StrategyPattern\CashRegister\Contracts\Payable;
use App\StrategyPattern\CashRegister\ElectronicReceipt;
use App\StrategyPattern\CashRegister\Contracts\Receiptable;
class CashContext
{
/**
* @var Payable
*/
private $discountMethod;
/**
* @var Receiptable
*/
private $receipt;
/**
* @param int $originalPrice
* @param string $discountType
* @param string $receiptType
*/
public function __construct($originalPrice, $discountType, $receiptType)
{
$this->resolveDiscountMethod($originalPrice, $discountType);
$this->resolveReceiptType($receiptType);
}
/**
* @param int $originalPrice
* @param string $discountType
*/
private function resolveDiscountMethod($originalPrice, $discountType)
{
switch ($discountType) {
case '20% off':
$this->discountMethod = new OffPercentPay($originalPrice, 0.2);
break;
case 'spend_300_feedback_100':
$this->discountMethod = new FeedbackPay($originalPrice, 300, 100);
break;
default:
$this->discountMethod = new NormalPay($originalPrice);
break;
}
}
/**
* @param string $receiptType
*/
private function resolveReceiptType($receiptType)
{
switch ($receiptType) {
case 'electronicReceipt':
$this->receipt = new ElectronicReceipt();
break;
default:
$this->receipt = new NormalReceipt();
break;
}
}
public function pay()
{
return $this->discountMethod->pay();
}
public function getReceipt()
{
return $this->receipt->getReceipt();
}
}
<?php
namespace App\StrategyPattern\CashRegister;
use App\StrategyPattern\CashRegister\CashContext;
class Program
{
/**
* @var CashContext
*/
private $cashContext;
/**
* @param int $originalPrice
* @param string $discountType
* @param string $receiptType
*/
public function __construct($originalPrice, $discountType, $receiptType)
{
$this->cashContext = new CashContext($originalPrice, $discountType, $receiptType);
}
public function pay()
{
return $this->cashContext->pay();
}
public function getReceipt()
{
return $this->cashContext->getReceipt();
}
}
[單一職責原則]
將類別本身職責跟算法族的職責分離,就是策略模式的精神!
[開放封閉原則]
這下子,我們終於不會在客戶提出一個新需求時,影響到全部的既有程式碼了。
[介面隔離原則]
定義出付錢介面與發票介面,讓兩者不會互相影響。
可以交由各自的算法族,分別實現。
[依賴反轉原則]
消費明細類別依賴抽象的付錢介面與發票介面。
不同的算法族,實現對應的抽象介面。
最後附上類別圖:
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)
ʕ •ᴥ•ʔ:使用策略模式,我們依然會做出許多小類別(算法族/算法),
但因為切分的更細,也就更能因應需求去做變化。